// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
//
//   Keepass2Android is free software: you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//   the Free Software Foundation, either version 3 of the License, or
//   (at your option) any later version.
//
//   Keepass2Android is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU General Public License for more details.
//
//   You should have received a copy of the GNU General Public License
//   along with Keepass2Android.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
#if !EXCLUDE_KEYTRANSFORM
using Android.App;
using Com.Keepassdroid.Database;
using Com.Keepassdroid.Database.Exception;
#endif
using Com.Keepassdroid.Database.Save;
using Java.Util;
using KeePass.Util;
using KeePassLib;
using KeePassLib.Cryptography;
using KeePassLib.Cryptography.Cipher;
using KeePassLib.Cryptography.KeyDerivation;
using KeePassLib.Interfaces;
using KeePassLib.Keys;
using KeePassLib.Security;
using PwIcon = KeePassLib.PwIcon;
using Random = System.Random;

namespace keepass2android
{
  public class KdbDatabaseFormat : IDatabaseFormat
  {
    private readonly IKp2aApp _app;
    private Dictionary<PwUuid, AdditionalGroupData> _groupData = new Dictionary<PwUuid, AdditionalGroupData>();
    private static readonly DateTime _expireNever = new DateTime(2999, 12, 28, 23, 59, 59);
    private List<PwEntryV3> _metaStreams;

    public KdbDatabaseFormat(IKp2aApp app)
    {
      _app = app;
    }

    public void PopulateDatabaseFromStream(PwDatabase db, Stream s, IStatusLogger slLogger)
    {
#if !EXCLUDE_KEYTRANSFORM
      var importer = new Com.Keepassdroid.Database.Load.ImporterV3();

      var hashingStream = new HashingStreamEx(s, false, new SHA256Managed());

      _metaStreams = new List<PwEntryV3>();

      string password = "";//no need to distinguish between null and "" because empty passwords are invalid (and null is not allowed)
      KcpPassword passwordKey = (KcpPassword)db.MasterKey.GetUserKey(typeof(KcpPassword));
      if (passwordKey != null)
      {
        password = passwordKey.Password.ReadString();
      }

      KcpKeyFile passwordKeyfile = (KcpKeyFile)db.MasterKey.GetUserKey(typeof(KcpKeyFile));
      MemoryStream keyfileStream = null;
      if (passwordKeyfile != null)
      {
        keyfileStream = new MemoryStream(passwordKeyfile.RawFileData.ReadData());
      }


      try
      {
        var dbv3 = importer.OpenDatabase(hashingStream, password, keyfileStream);

        db.Name = dbv3.Name;
        db.KdfParameters = (new AesKdf()).GetDefaultParameters();
        db.KdfParameters.SetUInt64(AesKdf.ParamRounds, (ulong)dbv3.NumKeyEncRounds);


        db.RootGroup = ConvertGroup(dbv3.RootGroup);
        if (dbv3.Algorithm == PwEncryptionAlgorithm.Rjindal)
        {
          db.DataCipherUuid = StandardAesEngine.AesUuid;
        }
        else
        {
          db.DataCipherUuid = new PwUuid(TwofishCipher.TwofishCipherEngine.TwofishCipherUuidBytes);
        }


      }
      catch (Java.IO.FileNotFoundException e)
      {
        throw new FileNotFoundException(
            ExceptionUtil.GetErrorMessage(e), e);
      }
      catch (Java.Lang.Exception e)
      {
        if (e.Message == "Invalid key!")
          throw new InvalidCompositeKeyException();
        throw new Exception(ExceptionUtil.GetErrorMessage(e) ??
                                                          e.GetType().Name, e);
      }

      HashOfLastStream = hashingStream.Hash;
      if (HashOfLastStream == null)
        throw new Exception("hashing didn't work"); //todo remove
#else
			throw new Exception("Kdb is excluded with Key transform!");
#endif
    }

#if !EXCLUDE_KEYTRANSFORM

    private PwGroup ConvertGroup(PwGroupV3 groupV3)
    {
      PwGroup pwGroup = new PwGroup(true, false);
      pwGroup.Uuid = CreateUuidFromGroupId(groupV3.Id.Id);

      //check if we have group data for this group already (from loading in a previous pass).
      //then use the same UUID (important for merging)
      var gdForGroup = _groupData.Where(g => g.Value.Id == groupV3.Id.Id).ToList();
      if (gdForGroup.Count == 1)
      {
        pwGroup.Uuid = gdForGroup.Single().Key;
      }

      pwGroup.Name = groupV3.Name;
      Android.Util.Log.Debug("KP2A", "load kdb: group " + groupV3.Name);
      pwGroup.CreationTime = ConvertTime(groupV3.TCreation);
      pwGroup.LastAccessTime = ConvertTime(groupV3.TLastAccess);
      pwGroup.LastModificationTime = ConvertTime(groupV3.TLastMod);
      pwGroup.ExpiryTime = ConvertTime(groupV3.TExpire);
      pwGroup.Expires = !(Math.Abs((pwGroup.ExpiryTime - _expireNever).TotalMilliseconds) < 500); ;

      if (groupV3.Icon != null)
        pwGroup.IconId = (PwIcon)groupV3.Icon.IconId;
      _groupData[pwGroup.Uuid] = new AdditionalGroupData
      {
        Flags = groupV3.Flags,
        Id = groupV3.Id.Id
      };


      for (int i = 0; i < groupV3.ChildGroups.Count; i++)
      {
        pwGroup.AddGroup(ConvertGroup(groupV3.GetGroupAt(i)), true);
      }
      for (int i = 0; i < groupV3.ChildEntries.Count; i++)
      {
        var entry = groupV3.GetEntryAt(i);
        if (entry.IsMetaStream)
        {
          _metaStreams.Add(entry);
          continue;
        }

        pwGroup.AddEntry(ConvertEntry(entry), true);
      }

      return pwGroup;
    }

    private PwUuid CreateUuidFromGroupId(int id)
    {
      byte[] template = new byte[] { 0xd2, 0x18, 0x22, 0x93,
                                           0x8e, 0xa4, 0x43, 0xf2,
                                           0xb4, 0xb5, 0x2a, 0x49,
                                           0x00, 0x00, 0x00, 0x00};
      byte[] idBytes = BitConverter.GetBytes(id);
      for (int i = 0; i < 4; i++)
      {
        template[i + 12] = idBytes[i];
      }
      return new PwUuid(template);
    }

    private PwEntry ConvertEntry(PwEntryV3 fromEntry)
    {
      PwEntry toEntry = new PwEntry(false, false);
      toEntry.Uuid = new PwUuid(fromEntry.Uuid.ToArray());
      String modTime = Android.Text.Format.DateFormat.GetTimeFormat(Application.Context).Format(fromEntry.TCreation.JDate);
      Android.Util.Log.Debug("KP2A", modTime);
      toEntry.CreationTime = ConvertTime(fromEntry.TCreation);
      toEntry.LastAccessTime = ConvertTime(fromEntry.TLastAccess);
      toEntry.LastModificationTime = ConvertTime(fromEntry.TLastMod);

      toEntry.ExpiryTime = ConvertTime(fromEntry.TExpire);

      toEntry.ExpiryTime = new DateTime(toEntry.ExpiryTime.Year, toEntry.ExpiryTime.Month, toEntry.ExpiryTime.Day, toEntry.ExpiryTime.Hour, toEntry.ExpiryTime.Minute, toEntry.ExpiryTime.Second);
      toEntry.Expires = !(Math.Abs((toEntry.ExpiryTime - _expireNever).TotalMilliseconds) < 500);

      if (fromEntry.Icon != null)
        toEntry.IconId = (PwIcon)fromEntry.Icon.IconId;
      SetFieldIfAvailable(toEntry, PwDefs.TitleField, false, fromEntry.Title);
      Android.Util.Log.Debug("KP2A", "load kdb: entry " + toEntry.Strings.ReadSafe(PwDefs.TitleField));
      SetFieldIfAvailable(toEntry, PwDefs.UserNameField, false, fromEntry.Username);
      SetFieldIfAvailable(toEntry, PwDefs.UrlField, false, fromEntry.Url);
      SetFieldIfAvailable(toEntry, PwDefs.PasswordField, true, fromEntry.Password);
      SetFieldIfAvailable(toEntry, PwDefs.NotesField, true, fromEntry.Additional);

      if ((fromEntry.GetBinaryData() != null) && (fromEntry.GetBinaryData().Length > 0))
      {
        toEntry.Binaries.Set(fromEntry.BinaryDesc, new ProtectedBinary(true, fromEntry.GetBinaryData()));
      }
      return toEntry;
    }

    private void SetFieldIfAvailable(PwEntry pwEntry, string fieldName, bool makeProtected, string value)
    {
      if (value != null)
      {
        pwEntry.Strings.Set(fieldName, new ProtectedString(makeProtected, value));
      }

    }

    private DateTime ConvertTime(PwDate date)
    {
      if (date == null)
        return PwDefs.DtDefaultNow;

      return JavaTimeToCSharp(date.JDate.Time);
    }

    private DateTime JavaTimeToCSharp(long javatime)
    {
      try
      {
        var utcTime = new DateTime(1970, 1, 1).AddMilliseconds(javatime);
        return TimeZoneInfo.ConvertTimeFromUtc(utcTime, TimeZoneInfo.Local);
      }
      catch (ArgumentOutOfRangeException)
      {
        return DateTime.MinValue;
      }


    }
#endif
    public byte[] HashOfLastStream { get; private set; }
    public bool CanWrite { get { return true; } }
    public string SuccessMessage { get { return null; } }

    public void Save(PwDatabase kpDatabase, Stream stream)
    {
      PwDatabaseV3 db = new PwDatabaseV3();
      KcpPassword pwd = kpDatabase.MasterKey.GetUserKey<KcpPassword>();
      string password = pwd != null ? pwd.Password.ReadString() : "";
      KcpKeyFile keyfile = kpDatabase.MasterKey.GetUserKey<KcpKeyFile>();
      Stream keyfileContents = null;
      if (keyfile != null)
      {
        keyfileContents = new MemoryStream(keyfile.RawFileData.ReadData());
      }
      db.SetMasterKey(password, keyfileContents);

      AesKdf kdf = new AesKdf();
      if (!kdf.Uuid.Equals(kpDatabase.KdfParameters.KdfUuid))
        db.NumRounds = (uint)PwDefs.DefaultKeyEncryptionRounds;
      else
      {
        ulong uRounds = kpDatabase.KdfParameters.GetUInt64(
            AesKdf.ParamRounds, PwDefs.DefaultKeyEncryptionRounds);
        uRounds = Math.Min(uRounds, 0xFFFFFFFEUL);

        db.NumRounds = (uint)uRounds;
      }


      db.Name = kpDatabase.Name;
      if (kpDatabase.DataCipherUuid.Equals(StandardAesEngine.AesUuid))
      {
        db.Algorithm = PwEncryptionAlgorithm.Rjindal;
      }
      else
      {
        db.Algorithm = PwEncryptionAlgorithm.Twofish;
      }

      //create groups
      db.Groups.Clear();
      var fromGroups = kpDatabase.RootGroup.GetGroups(true);
      Dictionary<int, PwGroupV3> groupV3s = new Dictionary<int, PwGroupV3>(fromGroups.Count());
      foreach (PwGroup g in fromGroups)
      {
        if (g == kpDatabase.RootGroup)
          continue;
        PwGroupV3 groupV3 = ConvertGroup(g, db);
        db.Groups.Add(groupV3);
        groupV3s[groupV3.Id.Id] = groupV3;
      }

      //traverse again and assign parents
      db.RootGroup = ConvertGroup(kpDatabase.RootGroup, db);
      db.RootGroup.Level = -1;

      AssignParent(kpDatabase.RootGroup, db, groupV3s);

      foreach (PwEntry e in kpDatabase.RootGroup.GetEntries(true))
      {
        PwEntryV3 entryV3 = ConvertEntry(e, db);
        entryV3.Parent = groupV3s[_groupData[e.ParentGroup.Uuid].Id];
        entryV3.Parent.ChildEntries.Add(entryV3);
        entryV3.GroupId = entryV3.Parent.Id.Id;
        db.Entries.Add(entryV3);
      }

      //add meta stream entries:
      if (db.Groups.Any())
      {
        foreach (var metaEntry in _metaStreams)
        {
          metaEntry.GroupId = db.Groups.First().Id.Id;
          db.Entries.Add(metaEntry);
        }

      }


      HashingStreamEx hashedStream = new HashingStreamEx(stream, true, null);
      PwDbV3Output output = new PwDbV3Output(db, hashedStream);
      output.Output();
      hashedStream.Close();
      HashOfLastStream = hashedStream.Hash;

      kpDatabase.HashOfLastIO = kpDatabase.HashOfFileOnDisk = HashOfLastStream;
      stream.Close();
    }

    public bool CanHaveEntriesInRootGroup
    {
      get { return false; }
    }

    public bool CanHaveMultipleAttachments
    {
      get { return false; }
    }

    public bool CanHaveCustomFields
    {
      get { return false; }
    }

    public bool HasDefaultUsername
    {
      get { return false; }
    }

    public bool HasDatabaseName
    {
      get { return false; }
    }

    public bool SupportsAttachmentKeys
    {
      get { return false; }
    }

    public bool SupportsTags
    {
      get { return false; }
    }

    public bool SupportsOverrideUrl
    {
      get { return false; }
    }

    public bool CanRecycle
    {
      get { return false; }
    }

    public bool SupportsTemplates
    {
      get { return false; }
    }

    private void AssignParent(PwGroup kpParent, PwDatabaseV3 dbV3, Dictionary<int, PwGroupV3> groupV3s)
    {
      PwGroupV3 parentV3;
      if (kpParent.ParentGroup == null)
      {
        parentV3 = dbV3.RootGroup;
      }
      else
      {
        parentV3 = groupV3s[_groupData[kpParent.Uuid].Id];
      }

      foreach (PwGroup g in kpParent.Groups.OrderBy(g => g.Name))
      {
        PwGroupV3 groupV3 = groupV3s[_groupData[g.Uuid].Id];

        parentV3.ChildGroups.Add(groupV3);
        groupV3.Parent = parentV3;

        AssignParent(g, dbV3, groupV3s);
      }
    }

    private PwGroupV3 ConvertGroup(PwGroup fromGroup, PwDatabaseV3 dbTo)
    {
      PwGroupV3 toGroup = new PwGroupV3();
      toGroup.Name = fromGroup.Name;

      toGroup.TCreation = new PwDate(ConvertTime(fromGroup.CreationTime));
      toGroup.TLastAccess = new PwDate(ConvertTime(fromGroup.LastAccessTime));
      toGroup.TLastMod = new PwDate(ConvertTime(fromGroup.LastModificationTime));
      if (fromGroup.Expires)
      {
        toGroup.TExpire = new PwDate(ConvertTime(fromGroup.ExpiryTime));
      }
      else
      {
        toGroup.TExpire = new PwDate(ConvertTime(_expireNever));
      }

      toGroup.Icon = dbTo.IconFactory.GetIcon((int)fromGroup.IconId);
      AdditionalGroupData groupData;
      if (_groupData.TryGetValue(fromGroup.Uuid, out groupData))
      {
        toGroup.Id = new PwGroupIdV3(groupData.Id);
        toGroup.Flags = groupData.Flags;
      }
      else
      {
        //group was added
        do
        {
          toGroup.Id = new PwGroupIdV3(new Random().Next());
        } while (_groupData.Values.Any(gd => gd.Id == toGroup.Id.Id)); //retry if id already exists
                                                                       //store to block new id and reuse when saving again (without loading in between)
        _groupData[fromGroup.Uuid] = new AdditionalGroupData
        {
          Id = toGroup.Id.Id
        };

      }

      return toGroup;
    }

    private PwEntryV3 ConvertEntry(PwEntry fromEntry, PwDatabaseV3 dbTo)
    {
      PwEntryV3 toEntry = new PwEntryV3();
      toEntry.Uuid = fromEntry.Uuid.UuidBytes;
      toEntry.CreationTime = ConvertTime(fromEntry.CreationTime);
      toEntry.LastAccessTime = ConvertTime(fromEntry.LastAccessTime);
      toEntry.LastModificationTime = ConvertTime(fromEntry.LastModificationTime);

      if (fromEntry.Expires)
      {
        toEntry.ExpiryTime = ConvertTime(fromEntry.ExpiryTime);
      }
      else
      {
        toEntry.ExpiryTime = ConvertTime(_expireNever);
      }


      toEntry.Icon = dbTo.IconFactory.GetIcon((int)fromEntry.IconId);
      toEntry.SetTitle(GetString(fromEntry, PwDefs.TitleField), dbTo);
      Android.Util.Log.Debug("KP2A", "save kdb: entry " + fromEntry.Strings.ReadSafe(PwDefs.TitleField));
      toEntry.SetUsername(GetString(fromEntry, PwDefs.UserNameField), dbTo);
      toEntry.SetUrl(GetString(fromEntry, PwDefs.UrlField), dbTo);
      var pwd = GetString(fromEntry, PwDefs.PasswordField);
      if (pwd != null)
        toEntry.SetPassword(pwd, dbTo);
      toEntry.SetNotes(GetString(fromEntry, PwDefs.NotesField), dbTo);
      if (fromEntry.Binaries.Any())
      {
        var binaryData = fromEntry.Binaries.First().Value.ReadData();
        toEntry.SetBinaryData(binaryData, 0, binaryData.Length);
      }

      return toEntry;
    }

    private string GetString(PwEntry fromEntry, string id)
    {
      ProtectedString protectedString = fromEntry.Strings.Get(id);
      if (protectedString == null)
        return "";
      return protectedString.ReadString();
    }

    private Date ConvertTime(DateTime dateTime)
    {
      long timestamp = (long)(dateTime.ToUniversalTime().Subtract(new DateTime(1970, 1, 1))).TotalMilliseconds;
      return new Date(timestamp);
    }
  }


  internal class AdditionalGroupData
  {
    public int Id { get; set; }
    public int Flags { get; set; }
  }
}